#!/usr/bin/python
#-*-coding: utf-8 -*-

"""This is the operating system interfaces of Daemon. 

It communicates with Cluster's Utilites, compiling the source code,
send the job to cluster and retrieve the job's results.

For each Cluster a class that implements the specific 
communication between Daemon and the Cluster must exist.

"""

import os
import platform

from subprocess import Popen, PIPE, STDOUT
from exceptions_ import ErroComando, ErroCompilacao, ErroJob, ErroOS

def setOS(parameters):
    """Verify the running OS and set the cluster object.

    For supported Windows systems, verify the OS version to define
    the class to instance.

    Return a cluster object 
    or None if the SO is not supported.

    """
    
    if platform.system() == 'Windows':    # Windows
        if platform.version()[0] == '5':   # 2003 is '5.2.something'
            sistema = Win2003
        elif platform.version()[0] == '6': # 2008 is '6.0.something'
            sistema = Win2008
    elif platform.system() == 'Linux':    # Linux
        sistema = Linux
    else: 
        print platform.system(), ' not Supported (yet).'
        return

    try:
     SO = sistema(parameters)
    except KeyError:                          # Se houve erro nas chaves do dict
     print 'Error: Check the config file.'  # com os parametros   
     return
    else:
     return SO


# Classes implementations
class SysOp(object):
    """The base class for the OS Deemons' interfaces.
    
    The follow methods are the minimal requerided to communicate the system
    with the Interop Router.
    
    """
      
    def __init__(self, config):
        """The Cluster Class Constructor.
        
        It receives a dict which contains the Deemon configs for
        the operating system.
        
        This items are necessary for any cluster type.
        
        """
        
        self.shareFolder = config['ShareFolder']
        self.NumNodes = config['Nodes']
        self.ProcessorsPerNode = config['PPN']
        self.id = config['ClusterID']
       
    def RunCommand(self, comando):
        """Function used for run commands inside the Deemon.
        
        With this function, the Deemon's enviroments variables can be preserved.
        The derived classes implement this function according to 
        the Operating System command line.
        
        Keyword arguments:
        
        comando -- The command call in the shell.
        
        Returns a string that contain the command output.
        
        """
       
        p = Popen(comando, stdout = PIPE, stderr = STDOUT, shell=True)
        output = p.communicate()[0]

        if p.returncode:
            #retorna o p.returncode para dar um sys.exit(rc)
            raise ErroComando(p.returncode)        
        return output
    
    def compileProject(self, project):
        """Method that compile the project source code.
        
        Keyword arguments:
        
        project -- a string with the Project ID
        
        Must be override by the derived class.
        
        """
        pass
    
    def submitJob(self, project, job):
        """Method taht submit the job to the Cluster and save the job ID.
        
        Keyword arguments:
        
        project -- a string with the Project ID
        
        job -- a string with the Job ID in the DB
        
        Return an int with the job ID in the cluster.
        
        Must be reimplemented in the derived class.
        
        """
        pass
    
    def jobIsComplete(self):
        """Verify de end of the job.
        
        True if the job has been finished, False if not. 
        
        Must be reimplemented in the derived class.
        
        """
        pass
     
#     def idleness(self):
#        return 0
    
class Windows(SysOp):
    """Implements the interface class with Windows systems."""
    
    __name__ = "Windows"
    
    def __init__(self, config):
        """Extends the SysOp.__init__ method, adding one more config parameter.
        
        In Windows it is necessary a password to submit jobs.
        
        """
        
        super(Windows, self).__init__(config)
        self.SysPasswd = config['SysPasswd']

    def submitJob(self, project, job):
        """Windows version of method for submit the job to Cluster.
        
        Override the SysOp method and return a int with the job id in
        the cluster system.
        
        """
        
        # CL syntax:
        # job submit /name:%s /numprocessors:%d /workdir:%s /stdout:%s.out /stderr:%s.err mpiexec %s.exe
        projDir = os.path.join(self.shareFolder, 'project%d' %(project.id))
        jobDir = os.path.join(projDir, 'job%d' %(job.id))
        
        aplication = os.path.join(projDir, 'program.exe')
        data = os.path.join(projDir, job.name)
        
        Comando = ('job submit /password:%(senha)s /name:%(Name)s \
                    /numprocessors:%(Nproc)s /workdir:%(Dir)s /stdout:%(Name)s.out \
                    /stderr:%(Name)s.err mpiexec %(Exe)s %(arguments)s %(data)s' 
                  %{'Name': job.name, 'Dir': jobDir, 'Nproc': project.reqProcessors, \
                    'arguments':project.parameters, 'Exe': aplication, 'data':data, \
                    'senha':self.SysPasswd})
        
        output = self.RunCommand(Comando)
        
        # Get the Job ID number.
        # The output is 
        # "Job has been submitted. ID: 177."
        # The number of job is the fifth word in the line
        # Ignore the end line point.
        return output.split()[5][:-1]     


    def jobIsComplete(self, job):
        """Windows implementation of method that verify if the job has finished.
        
        Overrides the SysOp method and return True or False
        if the job is ended or not.
        
        If the Job is cancelled or fail, throws the ErroJob Exception.
        
        """
        
        # CL syntax: job view %d > %s
        Comando = 'job view %s' %(job)
        output = self.RunCommand(Comando)
        
        # The Job status is the third word of the second line of output.
        output = output.splitlines()[1].split()[2]
        
        if output == "Finished":
            return True
        elif output == "Failed":
            raise ErroJob('FAIL')
        elif output == "Cancelled":
            raise ErroJob('CANCEL')
        else:
            return False
        
    def idleness(self):
        
        try:
            table = self.RunCommand('cluscfg view')
        except:
            return 0
        
        return table.splitlines()[9].split()[3]
  

class Win2003(Windows):
    """Implements Windows Server 2003 Cluster especific methods."""
    
    __name__ = "Windows Compute Cluster Server 2003"

    def compileProject(self, project):
        """Method for compile the project's source code in Windows 2003.
        
        Uses the auxiliar file *ambient.vs8.wccs.x86.bat* to set the especific
        enviroment variables used to complile the sorce code.
        
        If a compilation error occours, a ErroCompilacao Exception is raised
        
        """
        
        #cl /nologo /w \\hn\share\projectId\*.c /link /OUT:"\\hn\share\projectId\TesteM2.exe"
        dir = os.path.join(self.shareFolder, 'project%d' %(project))
        Comando = 'scripts\\compila.vs8.wccs.bat ' + dir + ' *.c ' + dir + ' program'
        
        try:
            output = self.RunCommand(Comando)   
                
            if (True): # mudar isso pra um modo verbose no futuro.
                print output
                
        except ErroComando, erro:
            print 'Compilation failed, check the source file and "ambient.vs8.wccs.x86.bat"'
            raise ErroCompilacao            
        
        

class Win2008(Windows):
    """Implements Windows Server 2008 Cluster especific methods."""
    
    __name__ = "Windows HPC Server 2008"

    def compileProject(self, project):
        """Method for compile the project's source code in Windows 2008.
        
        Uses the auxiliar file *ambient.vs9.whpc.bat* to set the especific
        enviroment variables used to complile the sorce code.
        
        If a compilation error occours, a ErroCompilacao Exception is raised
        
        """
        
        #cl /nologo /w \\hn\share\projectId\*.c /link /OUT:"\\hn\share\projectId\TesteM2.exe"
        dir = os.path.join(self.shareFolder, 'project%d' %(project))
        Comando = 'scripts\\compila.vs9.whpc.bat ' + dir + ' *.c ' + dir + ' program'
        
        try:
            output = self.RunCommand(Comando)  
            if (True): # mudar isso pra um modo verbose no futuro.
                print output
        except ErroComando, erro:
            print 'Compilation failed, check the source file and "ambient.vs9.whpc.x86.bat"'
            raise ErroCompilacao 
            



class Linux(SysOp):
    """Implements interface class with Linux systems.
    
    """
    
    __name__ = "Linux"
    
    def __init__(self, config):
          """Extends the SysOp.__init__ method, verifing if the user isn't root.
          
          """
          
          # here, we need check if user is the root
          if os.environ['LOGNAME'] == 'root':
                alert = """
                        =========================================
                        You cannot run Deemon using a root login.
                        Use a diferent user and try again.
                        =========================================
                        
                        """
                        
                print alert
                
                raise ErroOS('Linux')
          else:
                super(Linux, self).__init__(config)
            
    def compileProject(self, project):
        """Method for compile the project's source code in Linux.

        If a compilation error occours, a ErroCompilacao Exception is raised
        
        """
        
        # we must change directory before compile the source, else the .o files will be
        # saved in deemon folder and will occour a permission error
        path = os.path.join(self.shareFolder, 'project%s' %(project))
        old_path = os.getcwd()
        os.chdir(path)

        Comando = 'mpicc -w -o program *.c'

        try: 
            output = self.RunCommand(Comando)
        except ErroComando, erro:
              if erro.ReturnCode == 1:
                    raise ErroCompilacao
              else:
                    # this is not good and need be more specifc
                    print 'Command failed, check MPI library'
        finally:
              os.chdir(old_path)
        
    def submitJob(self, project, job):
        """Linux version of method for submit the job to Cluster."""
        
        # create submete.sh
        caminhoProjeto = os.path.join(self.shareFolder, 'project%s' %(project.id))
        caminhoJob = os.path.join(caminhoProjeto, 'job%s' %(job.id))
        scriptFile = os.path.join(caminhoJob, 'submete.sh')
        
        #create a submit script based in the template 
        script = open(scriptFile,'w')
        templateScript = open('scripts/submit.template')
        
        # use a dict to parse the keywords in the template
        params = {
                'nodes': self.NumNodes,
                'ppn': self.ProcessorsPerNode,
                'name': job.name,
                'jobPath': caminhoJob,
                'processors': project.reqProcessors,
                'execPath': caminhoProjeto,
                'execArgs': project.parameters,
                }
        
        for line in templateScript:
            script.write(line % params)
            
        script.close()
        templateScript.close()
        
        # CL syntax: qsub share/project/job/submete.sh
        Comando = 'qsub %s' %(scriptFile)

        try:
            output = self.RunCommand(Comando)
        except ErroComando, erro:
            print "Job Submit failed, check the UserName and other arguments"
            raise ErroJob('FAIL')
            
        # take JobID: 150.headnode.linux
        return int(output.split('.')[0])
    
    def jobIsComplete(self, job):
        """Linux implementation of method that verify if the job has finished."""
        
        # CL syntax: (sudo -u USER) qstat -n JOBID
        Comando = 'qstat -n %d' %(job)
        try:
            output = self.RunCommand(Comando)
        except ErroComando, erro:
            if erro.ReturnCode == 153:
                return True # ignora excecao, vai dar erro 153 quando der um qstat pra um job que jah acabou
            else:
                print 'Command Error in qstat, and it is not 153'
                
        return False

    def idleness(self):
        comando = 'pbsnodes -a'
        output = self.RunCommand(comando)
        ppn = int(self.ProcessorsPerNode)
        freeNodes = 0

        for linha in output.splitlines():
            if 'state = free' in linha:
                freeNodes += ppn
                
        return freeNodes

        
